package com.xuecheng.media.config;

import com.aliyun.green20220302.Client;
import com.aliyun.green20220302.models.DescribeUploadTokenResponse;
import com.aliyun.green20220302.models.DescribeUploadTokenResponseBody;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.model.PutObjectRequest;
import com.aliyun.teaopenapi.models.Config;
import lombok.extern.slf4j.Slf4j;

import java.io.File;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

/**
 * @author 勾新杰
 * @version 1.0
 * @description: 自己写得模拟单例，因为直接用bean的话，令牌过期后据需要换一个oss client，bean不能做到这一点
 * @date 2025/3/13 15:47
 */
@Slf4j
public class AliyunConfig {

    /**
     * 是否使用VPC
     */
    private static final boolean isVPC = false;

    /**
     * accessKeyId
     */
    private static final String ALIBABA_CLOUD_ACCESS_KEY_ID = "ALIBABA_CLOUD_ACCESS_KEY_ID";

    /**
     * accessKeySecret
     */
    private static final String ALIBABA_CLOUD_ACCESS_KEY_SECRET = "ALIBABA_CLOUD_ACCESS_KEY_SECRET";

    /**
     * 域名
     */
    private static final String ENDPOINT = "green-cip.cn-hangzhou.aliyuncs.com";

    /**
     * 视频审核客户端，这个是基于饿汉式的单例
     */
    private static final Client client;

    // volatile原理详解：
    // 1. 可见性
    // 2. 禁止指令重排序

    /**
     * OSS客户端<br>
     * 用volatile保证可见性，也就是oss客户端修改了就会立马同步看得到<br>
     * 用volatile保证有序性，防止优化器重排序，导致取到未完全初始化的对象<br>
     * 这个是基于懒汉式的单例，
     */
    private static volatile OSS ossClient;

    /**
     * token存储，用于复用oss客户端
     */
    private static final Map<String, DescribeUploadTokenResponseBody.DescribeUploadTokenResponseBodyData> tokenMap = new HashMap<>();

    // 初始化视频审核客户端
    static {
        try {
            Config config = new Config();
            config.setAccessKeyId(System.getenv(ALIBABA_CLOUD_ACCESS_KEY_ID));
            config.setAccessKeySecret(System.getenv(ALIBABA_CLOUD_ACCESS_KEY_SECRET));
            config.setEndpoint(ENDPOINT);
            client = new Client(config);
            refreshToken(); // 初始化时获取令牌
        } catch (Exception e) {
            log.error("初始化阿里云审核客户端失败", e);
            throw new RuntimeException(e);
        }
    }

    /**
     * 刷新token
     *
     * @throws Exception 异常
     */
    private static void refreshToken() throws Exception {
        DescribeUploadTokenResponse tokenResponse = client.describeUploadToken();
        tokenMap.put(ENDPOINT, tokenResponse.getBody().getData());
    }

    /**
     * 获取视频审核客户端
     *
     * @return Client
     */
    public static Client getGreenClient() {
        return client;
    }

    /**
     * <h2>剖析双重检查锁定：</h2>
     * <h3>1.其实synchronized配合代码块内检查也能做到：</h3>
     * <pre>{@code
     * public static synchronized Singleton getInstance() {
     *      if (INSTANCE == null) {
     *          INSTANCE = new Singleton3();
     *      }
     *      return INSTANCE;
     *  }
     * }</pre>
     * <p>但是这种做法有以下两个缺点：
     * <ul>
     *     <li>锁的粒度太大</li>
     *     <li>当实例初始化好了或者对应实例没有过期，还是会去获取锁，增加开销</li>
     * </ul>
     * </p>
     * <h3>2.第一次检查的意义：</h3>
     * <ul>
     *     <li>直接判断单例是否存在或者没有过期，如果满足条件，直接返回，读操作不用考虑线程安全问题</li>
     * </ul>
     * <h3>3.加synchronize锁的意义：</h3>
     * <ul>
     *     <li>针对于创建单例操作进行加锁，细化锁的粒度</li>
     *     <li>保证创建单例的过程是不会发生代码交叉的，只能一个一个执行完。</li>
     *     <li>但是第一次检查不是线程安全的，可能会有多个线程进入第一次检查内，虽然不会交叉着并发执行，但是会以某种顺序依次执行创建单例的操作</li>
     *     <li>所以为了保证单例性，需要加入第二次检查，确保只创建一个单例</li>
     * </ul>
     * <h3>4.第二次检查的意义：</h3>
     * <ul>
     *     <li>确保多个线程依次拿到锁进行创建实例的时候只创建一个单例</li>
     * </ul>
     */
    public static void getOssClient() {
        // 如果ossClient是空后者令牌过期了，重新创建oss客户端
        DescribeUploadTokenResponseBody.DescribeUploadTokenResponseBodyData tokenData = tokenMap.get(ENDPOINT);
        long currTime = System.currentTimeMillis() / 1000;
        // 双重检查锁定，实现单例模式。这里是第一次检查
        if (ossClient == null || tokenData.expiration <= currTime) {
            // 同步代码块
            synchronized (AliyunConfig.class) {
                tokenData = tokenMap.get(ENDPOINT);
                currTime = System.currentTimeMillis() / 1000;
                // 第二次检查
                if (ossClient == null || tokenData.expiration <= currTime) {
                    try {
                        // 更新token
                        refreshToken();
                        // 获取新的token
                        tokenData = tokenMap.get(ENDPOINT);
                        // 用新的token创建新的oss客户端
                        createNewOssClient(tokenData);
                    } catch (Exception e) {
                        log.error("刷新OSS客户端失败", e);
                        throw new RuntimeException(e);
                    }
                }
            }
        }
    }

    /**
     * 创建新的OSS客户端
     *
     * @param tokenData token数据
     */
    private static void createNewOssClient(DescribeUploadTokenResponseBody.DescribeUploadTokenResponseBodyData tokenData) {
        // 1. 创建新的OSS客户端
        OSS newClient = isVPC
                ? new OSSClientBuilder().build(
                tokenData.getOssInternalEndPoint(),
                tokenData.getAccessKeyId(),
                tokenData.getAccessKeySecret(),
                tokenData.getSecurityToken())
                : new OSSClientBuilder().build(
                tokenData.getOssInternetEndPoint(),
                tokenData.getAccessKeyId(),
                tokenData.getAccessKeySecret(),
                tokenData.getSecurityToken());
        // 2. 旧的客户端关闭
        if (ossClient != null) {
            try {
                ossClient.shutdown();
            } catch (Exception e) {
                log.warn("关闭旧OSS客户端失败", e);
            }
        }
        // 3. 设置新的oss客户端
        ossClient = newClient;
    }

    /**
     * 上传文件
     *
     * @param filePath 文件路径
     */
    public static String uploadFile(String filePath) {
        // 1. 获取oss客户端
        getOssClient();
        DescribeUploadTokenResponseBody.DescribeUploadTokenResponseBodyData tokenData = tokenMap.get(ENDPOINT);
        String[] split = filePath.split("\\.");
        // 2. 组装文件名
        String objectName;
        if (split.length > 1) {
            objectName = tokenData.getFileNamePrefix() + UUID.randomUUID() + "." + split[split.length - 1];
        } else {
            objectName = tokenData.getFileNamePrefix() + UUID.randomUUID();
        }
        // 3. 上传文件
        PutObjectRequest putObjectRequest = new PutObjectRequest(tokenData.getBucketName(), objectName, new File(filePath));
        ossClient.putObject(putObjectRequest);
        return objectName;
    }

    /**
     * 获取上传token
     *
     * @return DescribeUploadTokenResponseBody.DescribeUploadTokenResponseBodyData
     */
    public static DescribeUploadTokenResponseBody.DescribeUploadTokenResponseBodyData getToken() {
        getOssClient();
        return tokenMap.get(ENDPOINT);
    }
}
